Iterationsprotokolle
Iterationsprotokolle sind keine neuen Built-ins oder Syntaxelemente, sondern Protokolle. Diese Protokolle können von jedem Objekt durch das Befolgen bestimmter Konventionen implementiert werden.
Es gibt zwei Protokolle: Das iterable Protokoll und das iterator Protokoll.
Das iterable Protokoll
Das iterable Protokoll erlaubt es JavaScript-Objekten, ihr Iterationsverhalten zu definieren oder anzupassen, zum Beispiel welche Werte in einer for...of
-Konstruktion durchlaufen werden. Einige eingebaute Typen sind eingebaute Iterables mit einem Standard-Iterationsverhalten, wie zum Beispiel Array
oder Map
, während andere Typen (wie zum Beispiel Object
) dies nicht sind.
Um iterierbar zu sein, muss ein Objekt die Methode [Symbol.iterator]()
implementieren, was bedeutet, dass das Objekt (oder eines der Objekte in seiner Prototypenkette) eine Eigenschaft mit einem [Symbol.iterator]
-Schlüssel haben muss, die über den konstanten Symbol.iterator
verfügbar ist:
[Symbol.iterator]()
-
Eine Funktion ohne Argumente, die ein Objekt zurückgibt, das dem Iterator-Protokoll entspricht.
Wann immer ein Objekt iteriert werden muss (zum Beispiel zu Beginn einer for...of
-Schleife), wird seine [Symbol.iterator]()
-Methode ohne Argumente aufgerufen, und der zurückgegebene Iterator wird verwendet, um die zu durchlaufenden Werte zu erhalten.
Beachten Sie, dass, wenn diese Funktion ohne Argumente aufgerufen wird, sie als Methode auf dem iterierbaren Objekt aufgerufen wird. Deshalb kann innerhalb der Funktion das this
-Schlüsselwort verwendet werden, um auf die Eigenschaften des iterierbaren Objekts zuzugreifen und zu entscheiden, was während der Iteration bereitgestellt werden soll.
Diese Funktion kann eine normale Funktion sein oder eine Generatorfunktion, sodass beim Aufruf ein Iterator-Objekt zurückgegeben wird. Innerhalb dieser Generatorfunktion kann jeder Eintrag durch die Verwendung von yield
bereitgestellt werden.
Das iterator Protokoll
Das iterator Protokoll definiert eine standardisierte Methode zur Erzeugung einer Sequenz von Werten (entweder endlich oder unendlich) und potenziell eines Rückgabewerts, wenn alle Werte erzeugt wurden.
Ein Objekt ist ein Iterator, wenn es eine next()
-Methode mit den folgenden Semantiken implementiert:
next()
-
Eine Funktion, die null oder ein Argument akzeptiert und ein Objekt zurückgibt, das der
IteratorResult
-Schnittstelle entspricht (siehe unten). Wenn ein nicht-Objekt-Wert (wiefalse
oderundefined
) zurückgegeben wird, während ein eingebautes Sprachfeature (wiefor...of
) den Iterator verwendet, wird einTypeError
("iterator.next() returned a non-object value"
) ausgelöst.
Alle Methoden des Iterator-Protokolls (next()
, return()
, und throw()
) sollen ein Objekt zurückgeben, das die IteratorResult
-Schnittstelle implementiert. Es muss die folgenden Eigenschaften haben:
done
Optional-
Ein Boolean, der
false
ist, wenn der Iterator den nächsten Wert in der Sequenz erzeugen konnte. (Das ist gleichbedeutend damit, die Eigenschaftdone
überhaupt nicht anzugeben.)Hat den Wert
true
, wenn der Iterator seine Sequenz abgeschlossen hat. In diesem Fall gibtvalue
optional den Rückgabewert des Iterators an. value
Optional-
Jeder beliebige JavaScript-Wert, der vom Iterator zurückgegeben wird. Kann weggelassen werden, wenn
done
true
ist.
In der Praxis ist keine der Eigenschaften strikt erforderlich; wenn ein Objekt ohne eine der beiden Eigenschaften zurückgegeben wird, ist es im Wesentlichen gleichbedeutend mit { done: false, value: undefined }
.
Wenn ein Iterator ein Ergebnis mit done: true
zurückgibt, wird erwartet, dass alle nachfolgenden Aufrufe von next()
ebenfalls done: true
zurückgeben, obwohl dies auf Sprachebene nicht erzwungen wird.
Die next
-Methode kann einen Wert empfangen, der dem Methodenrumpf zur Verfügung gestellt wird. Kein eingebautes Sprachfeature wird einen Wert übergeben. Der Wert, der an die next
-Methode von Generatoren übergeben wird, wird zum Wert des entsprechenden yield
-Ausdrucks.
Optional kann der Iterator auch die return(value)
- und throw(exception)
-Methoden implementieren. Wenn diese aufgerufen werden, teilt der Iterator mit, dass der Aufrufer mit der Iteration fertig ist und alle notwendigen Bereinigungsaktionen durchführen kann (wie das Schließen einer Datenbankverbindung).
return(value)
Optional-
Eine Funktion, die null oder ein Argument akzeptiert und ein Objekt zurückgibt, das der
IteratorResult
-Schnittstelle entspricht, typischerweise mitvalue
gleich dem übergebenenvalue
unddone
gleichtrue
. Der Aufruf dieser Methode teilt dem Iterator mit, dass der Aufrufer nicht beabsichtigt, weiterenext()
-Aufrufe zu tätigen und kann Aufräumaktionen durchführen. Wenn eingebaute Sprachmerkmalereturn()
zur Bereinigung aufrufen, istvalue
immerundefined
. throw(exception)
Optional-
Eine Funktion, die null oder ein Argument akzeptiert und ein Objekt zurückgibt, das der
IteratorResult
-Schnittstelle entspricht, typischerweise mitdone
gleichtrue
. Der Aufruf dieser Methode teilt dem Iterator mit, dass der Aufrufer eine Fehlerbedingung erkennt, undexception
ist typischerweise eineError
-Instanz. Kein eingebautes Sprachmerkmal ruftthrow()
zu Bereinigungszwecken auf - es ist eine spezielle Funktion von Generatoren für die Symmetrie vonreturn
/throw
.
Hinweis:
Es ist nicht möglich, reflektierend (d.h. ohne tatsächlich next()
aufzurufen und das zurückgegebene Ergebnis zu validieren) zu wissen, ob ein bestimmtes Objekt das Iterator-Protokoll implementiert.
Es ist sehr einfach, einen Iterator auch iterierbar zu machen: Implementieren Sie einfach eine [Symbol.iterator]()
-Methode, die this
zurückgibt.
// Satisfies both the Iterator Protocol and Iterable
const myIterator = {
next() {
// …
},
[Symbol.iterator]() {
return this;
},
};
Ein solches Objekt wird als iterierbarer Iterator bezeichnet. Dadurch kann ein Iterator von den verschiedenen Syntaxen konsumiert werden, die Iterables erwarten – daher ist es selten nützlich, das Iterator-Protokoll zu implementieren, ohne auch Iterable zu implementieren. (Tatsächlich erwarten fast alle Syntaxen und APIs Iterables, nicht Iteratoren.) Das Generator-Objekt ist ein Beispiel:
const aGeneratorObject = (function* () {
yield 1;
yield 2;
yield 3;
})();
console.log(typeof aGeneratorObject.next);
// "function" — it has a next method (which returns the right result), so it's an iterator
console.log(typeof aGeneratorObject[Symbol.iterator]);
// "function" — it has an [Symbol.iterator] method (which returns the right iterator), so it's an iterable
console.log(aGeneratorObject[Symbol.iterator]() === aGeneratorObject);
// true — its [Symbol.iterator] method returns itself (an iterator), so it's an iterable iterator
Alle eingebauten Iteratoren erben von Iterator.prototype
, das die [Symbol.iterator]()
-Methode als Rückgabe von this
implementiert, sodass eingebaute Iteratoren ebenfalls iterierbar sind.
Es ist jedoch besser, wenn möglich, dass iterable[Symbol.iterator]()
verschiedene Iteratoren zurückgibt, die immer von vorne beginnen, wie es zum Beispiel Set.prototype[Symbol.iterator]()
tut.
Die asynchronen Iterator- und asynchronen Iterierbaren-Protokolle
Es gibt ein weiteres Paar von Protokollen für die asynchrone Iteration, genannt asynchrone Iterator- und asynchrone Iterierbaren-Protokolle. Sie haben sehr ähnliche Schnittstellen im Vergleich zu den iterierbaren und Iterator-Protokollen, mit der Ausnahme, dass jeder Rückgabewert von den Aufrufen der Iterator-Methoden in einem Promise eingeschlossen ist.
Ein Objekt implementiert das asynchrone Iterierbaren-Protokoll, wenn es die folgenden Methoden implementiert:
[Symbol.asyncIterator]()
-
Eine Funktion ohne Argumente, die ein Objekt zurückgibt, das dem asynchronen Iterator-Protokoll entspricht.
Ein Objekt implementiert das asynchrone Iterator-Protokoll, wenn es die folgenden Methoden implementiert:
next()
-
Eine Funktion, die null oder ein Argument akzeptiert und ein Promise zurückgibt. Das Promise wird mit einem Objekt erfüllt, das der
IteratorResult
-Schnittstelle entspricht, und die Eigenschaften haben dieselbe Semantik wie die des synchronen iterators. return(value)
Optional-
Eine Funktion, die null oder ein Argument akzeptiert und ein Promise zurückgibt. Das Promise wird mit einem Objekt erfüllt, das der
IteratorResult
-Schnittstelle entspricht, und die Eigenschaften haben dieselbe Semantik wie die des synchronen iterators. throw(exception)
Optional-
Eine Funktion, die null oder ein Argument akzeptiert und ein Promise zurückgibt. Das Promise wird mit einem Objekt erfüllt, das der
IteratorResult
-Schnittstelle entspricht, und die Eigenschaften haben dieselbe Semantik wie die des synchronen iterators.
Interaktionen zwischen der Sprache und Iterationsprotokollen
Die Sprache definiert APIs, die entweder Iterables oder Iteratoren produzieren oder konsumieren.
Eingebaute Iterables
String
, Array
, TypedArray
, Map
, Set
, und Segments
(erstellt von Intl.Segmenter.prototype.segment()
) sind alle eingebaute Iterables, weil jedes ihrer prototype
-Objekte eine [Symbol.iterator]()
-Methode implementiert. Zusätzlich sind das arguments
-Objekt und einige DOM-Sammlungstypen wie NodeList
ebenfalls iterierbar.
Es gibt kein Objekt in der Kern-JavaScript-Sprache, das asynchron iterierbar ist. Einige Web-APIs, wie etwa ReadableStream
, haben die Symbol.asyncIterator
-Methode standardmäßig gesetzt.
Generator-Funktionen geben Generator-Objekte zurück, die iterierbare Iteratoren sind. Async-Generator-Funktionen geben asynchrone Generatorobjekte zurück, die asynchrone iterierbare Iteratoren sind.
Die von eingebauten Iterables zurückgegebenen Iteratoren erben tatsächlich alle von einer gemeinsamen Klasse Iterator
, die die oben erwähnte Methode [Symbol.iterator]() { return this; }
implementiert und sie somit alle zu iterierbaren Iteratoren macht. Die Iterator
-Klasse bietet auch zusätzliche Hilfsmethoden zusätzlich zur next()
-Methode, die vom Iterator-Protokoll gefordert wird. Sie können die Prototypenkette eines Iterators prüfen, indem Sie sie in einer grafischen Konsole protokollieren.
console.log([][Symbol.iterator]()); Array Iterator {} [[Prototype]]: Array Iterator ==> This is the prototype shared by all array iterators next: ƒ next() Symbol(Symbol.toStringTag): "Array Iterator" [[Prototype]]: Object ==> This is the prototype shared by all built-in iterators Symbol(Symbol.iterator): ƒ [Symbol.iterator]() [[Prototype]]: Object ==> This is Object.prototype
Eingebaute APIs, die Iterables akzeptieren
Es gibt viele APIs, die Iterables akzeptieren. Einige Beispiele sind:
Map()
WeakMap()
Set()
WeakSet()
Promise.all()
Promise.allSettled()
Promise.race()
Promise.any()
Array.from()
Object.groupBy()
Map.groupBy()
const myObj = {};
new WeakSet(
(function* () {
yield {};
yield myObj;
yield {};
})(),
).has(myObj); // true
Syntaxen, die Iterables erwarten
Einige Anweisungen und Ausdrücke erwarten Iterables, zum Beispiel for...of
-Schleifen, Array und Parameterverteilung, yield*
, und Array-Destrukturierung:
for (const value of ["a", "b", "c"]) {
console.log(value);
}
// "a"
// "b"
// "c"
console.log([..."abc"]); // ["a", "b", "c"]
function* gen() {
yield* ["a", "b", "c"];
}
console.log(gen().next()); // { value: "a", done: false }
[a, b, c] = new Set(["a", "b", "c"]);
console.log(a); // "a"
Wenn eingebauter Syntax ein Iterator iteriert und das letzte Ergebnis done
false
ist (d.h. der Iterator kann mehr Werte erzeugen), aber keine weiteren Werte benötigt werden, wird die return
-Methode aufgerufen, falls vorhanden. Dies kann zum Beispiel passieren, wenn ein break
oder return
in einer for...of
-Schleife getroffen wird oder wenn alle Bezeichner bereits in einer Array-Destrukturierung gebunden sind.
const obj = {
[Symbol.iterator]() {
let i = 0;
return {
next() {
i++;
console.log("Returning", i);
if (i === 3) return { done: true, value: i };
return { done: false, value: i };
},
return() {
console.log("Closing");
return { done: true };
},
};
},
};
const [a] = obj;
// Returning 1
// Closing
const [b, c, d] = obj;
// Returning 1
// Returning 2
// Returning 3
// Already reached the end (the last call returned `done: true`),
// so `return` is not called
console.log([b, c, d]); // [1, 2, undefined]; the value associated with `done: true` is not reachable
for (const b of obj) {
break;
}
// Returning 1
// Closing
Die for await...of
-Schleife und yield*
in async-Generator-Funktionen (aber nicht in synchronen Generator-Funktionen) sind die einzigen Möglichkeiten, mit asynchronen Iterables zu interagieren. Die Verwendung von for...of
, Array-Verteilung usw. auf einem asynchronen Iterable, das nicht auch ein synchrones Iterable ist (d.h. es hat [Symbol.asyncIterator]()
aber kein [Symbol.iterator]()
), wird einen TypeError auslösen: x ist nicht iterierbar.
Fehlerbehandlung
Da Iteration die Übertragung der Kontrolle zwischen dem Iterator und dem Verbraucher umfasst, findet die Fehlerbehandlung in beide Richtungen statt: wie der Verbraucher Fehler behandelt, die vom Iterator geworfen werden, und wie der Iterator Fehler behandelt, die vom Verbraucher geworfen werden. Wenn Sie eine der eingebauten Methoden der Iteration verwenden, kann die Sprache auch Fehler werfen, weil das Iterable bestimmte Invarianten verletzt. Wir werden beschreiben, wie eingebaute Syntaxen Fehler erzeugen und behandeln, was als Richtlinie für Ihren eigenen Code verwendet werden kann, wenn Sie den Iterator manuell durchlaufen.
Nicht wohlgeformte Iterables
Fehler können beim Abrufen des Iterators aus dem Iterable auftreten. Die hier durchgesetzte Sprachinvariante ist, dass das Iterable einen gültigen Iterator erzeugen muss:
- Es hat eine aufrufbare
[Symbol.iterator]()
-Methode. - Die
[Symbol.iterator]()
-Methode gibt ein Objekt zurück. - Das Objekt, das von
[Symbol.iterator]()
zurückgegeben wird, hat eine aufrufbarenext()
-Methode.
Bei der Verwendung eingebauter Syntax zur Initiierung der Iteration über ein nicht wohlgeformtes Iterable wird ein TypeError ausgelöst.
const nonWellFormedIterable = { [Symbol.iterator]: 1 };
[...nonWellFormedIterable]; // TypeError: nonWellFormedIterable is not iterable
nonWellFormedIterable[Symbol.iterator] = () => 1;
[...nonWellFormedIterable]; // TypeError: [Symbol.iterator]() returned a non-object value
nonWellFormedIterable[Symbol.iterator] = () => ({});
[...nonWellFormedIterable]; // TypeError: nonWellFormedIterable[Symbol.iterator]().next is not a function
Für asynchrone Iterables, wenn ihr [Symbol.asyncIterator]()
-Eigenschaftswert undefined
oder null
ist, fällt JavaScript zurück auf die Verwendung der [Symbol.iterator]
-Eigenschaft stattdessen (und wandelt den resultierenden Iterator in einen asynchronen Iterator um, indem es die Methoden weiterleitet). Andernfalls muss die [Symbol.asyncIterator]
-Eigenschaft auch den oben genannten Invarianten entsprechen.
Diese Art von Fehlern kann vermieden werden, indem das Iterable zuerst validiert wird, bevor versucht wird, es zu iterieren. Es ist jedoch ziemlich selten, da Sie normalerweise den Typ des Objekts kennen, über das Sie iterieren. Wenn Sie dieses Iterable von einem anderen Code erhalten, sollten Sie den Fehler einfach zum Aufrufer weiterleiten, damit dieser weiß, dass eine ungültige Eingabe bereitgestellt wurde.
Fehler während der Iteration
Die meisten Fehler treten auf, wenn der Iterator weitergeschaltet wird (Aufruf von next()
). Die hier durchgesetzte Sprachinvariante ist, dass die next()
-Methode ein Objekt zurückgeben muss (für asynchrone Iteratoren ein Objekt nach dem Warten). Andernfalls wird ein TypeError ausgelöst.
Wenn die Invariante verletzt wird oder die next()
-Methode einen Fehler wirft (für asynchrone Iteratoren kann sie auch ein abgelehntes Promise zurückgeben), wird der Fehler an den Aufrufer weitergeleitet. Für eingebaute Syntaxen wird die in Bearbeitung befindliche Iteration ohne erneutes Versuchen oder Bereinigung abgebrochen (mit der Annahme, dass, wenn die next()
-Methode den Fehler geworfen hat, sie bereits bereinigt hat). Wenn Sie next()
manuell aufrufen, können Sie den Fehler abfangen und erneut next()
aufrufen, aber im Allgemeinen sollten Sie davon ausgehen, dass der Iterator bereits geschlossen ist.
Wenn der Aufrufer beschließt, die Iteration aus einem anderen Grund als den Fehlern im vorherigen Absatz zu beenden, zum Beispiel, wenn er in seinem eigenen Code einen Fehlerzustand betritt (zum Beispiel beim Verarbeiten eines ungültigen Werts, der vom Iterator erzeugt wurde), sollte er die return()
-Methode des Iterators aufrufen, falls vorhanden. Dies ermöglicht es dem Iterator, alle notwendigen Aufräumarbeiten durchzuführen. Die return()
-Methode wird nur für vorzeitige Beendigungen aufgerufen - wenn next()
done: true
zurückgibt, wird die return()
-Methode nicht aufgerufen, mit der Annahme, dass der Iterator bereits bereinigt hat.
Die return()
-Methode kann ebenfalls ungültig sein! Die Sprache setzt ebenfalls durch, dass die return()
-Methode ein Objekt zurückgeben muss, und wirft andernfalls einen TypeError. Wenn die return()
-Methode einen Fehler wirft, wird der Fehler an den Aufrufer weitergeleitet. Sollte die return()
-Methode jedoch aufgerufen werden, weil der Aufrufer in seinem eigenen Code auf einen Fehler gestoßen ist, überschreibt dieser Fehler den von der return()
-Methode geworfenen Fehler.
Normalerweise implementiert der Aufrufer die Fehlerbehandlung so:
try {
for (const value of iterable) {
// …
}
} catch (e) {
// Handle the error
}
Der catch
kann Fehler abfangen, die geworfen werden, wenn iterable
kein gültiges Iterable ist, wenn next()
einen Fehler wirft, wenn return()
einen Fehler wirft (falls die for
-Schleife vorzeitig beendet wird), und wenn der for
-Schleifenrumpf einen Fehler wirft.
Die meisten Iteratoren werden mit Generatorfunktionen implementiert, sodass wir demonstrieren, wie Generatorfunktionen typischerweise Fehler behandeln:
function* gen() {
try {
yield doSomething();
yield doSomethingElse();
} finally {
cleanup();
}
}
Das Fehlen von catch
führt dazu, dass Fehler, die von doSomething()
oder doSomethingElse()
geworfen werden, an den Aufrufer von gen
weitergeleitet werden. Wenn diese Fehler innerhalb der Generatorfunktion abgefangen werden (was ebenso ratsam ist), kann die Generatorfunktion entscheiden, ob sie weiterhin Werte erzeugt oder vorzeitig beendet wird. Der finally
-Block ist jedoch notwendig für Generatoren, die offene Ressourcen verwalten. Der finally
-Block wird garantiert ausgeführt, entweder wenn das letzte next()
aufgerufen wird oder wenn return()
aufgerufen wird.
Fehlerweiterleitung
Einige eingebaute Syntaxen wickeln einen Iterator in einen anderen Iterator ein. Dazu gehören der von Iterator.from()
erzeugte Iterator, iterator helper methods (map()
, filter()
, take()
, drop()
, und flatMap()
), yield*
, und ein versteckter Wrapper, wenn Sie asynchrone Iteration (for await...of
, Array.fromAsync
) auf synchronen Iteratoren verwenden. Der ummantelte Iterator ist dann dafür verantwortlich, Fehler zwischen dem internen Iterator und dem Aufrufer weiterzuleiten.
- Alle Wrapper-Iteratoren leiten die
next()
-Methode des internen Iterators direkt weiter, einschließlich deren Rückgabewerte und ausgelöster Fehler. - Wrapper-Iteratoren leiten im Allgemeinen die
return()
-Methode des internen Iterators direkt weiter. Wenn diereturn()
-Methode beim internen Iterator nicht existiert, gibt er stattdessen{ done: true, value: undefined }
zurück. Bei Iterator-Helpmethoden: wenn dienext()
-Methode des Iterator-Helpers noch nicht aufgerufen wurde, gibt der aktuelle Iterator, nachdem er versucht hat,return()
auf dem internen Iterator aufzurufen, immer{ done: true, value: undefined }
zurück. Dies ist konsistent mit den Generatorfunktionen, deren Ausführung noch nicht in denyield*
-Ausdruck eingetreten ist. yield*
ist die einzige eingebaute Syntax, die diethrow()
-Methode des internen Iterators weiterleitet. Weitere Informationen zur Weiterleitung derreturn()
- undthrow()
-Methoden durchyield*
, finden Sie in der entsprechenden Referenz.
Beispiele
Benutzerdefinierte Iterables
Sie können Ihre eigenen Iterables wie folgt erstellen:
const myIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
},
};
console.log([...myIterable]); // [1, 2, 3]
Einfacher Iterator
Iteratoren sind von Natur aus zustandsbehaftet. Wenn Sie ihn nicht als Generatorfunktion definieren (wie das obige Beispiel zeigt), möchten Sie wahrscheinlich den Zustand in einem Closure kapseln.
function makeIterator(array) {
let nextIndex = 0;
return {
next() {
return nextIndex < array.length
? {
value: array[nextIndex++],
done: false,
}
: {
done: true,
};
},
};
}
const it = makeIterator(["yo", "ya"]);
console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done); // true
Unendlicher Iterator
function idMaker() {
let index = 0;
return {
next() {
return {
value: index++,
done: false,
};
},
};
}
const it = idMaker();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// …
Ein Iterable mit einem Generator definieren
function* makeGenerator(array) {
let nextIndex = 0;
while (nextIndex < array.length) {
yield array[nextIndex++];
}
}
const gen = makeGenerator(["yo", "ya"]);
console.log(gen.next().value); // 'yo'
console.log(gen.next().value); // 'ya'
console.log(gen.next().done); // true
function* idMaker() {
let index = 0;
while (true) {
yield index++;
}
}
const it = idMaker();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// …
Ein Iterable mit einer Klasse definieren
Die Zustandseinkapselung kann auch mit privaten Eigenschaften durchgeführt werden.
class SimpleClass {
#data;
constructor(data) {
this.#data = data;
}
[Symbol.iterator]() {
// Use a new index for each iterator. This makes multiple
// iterations over the iterable safe for non-trivial cases,
// such as use of break or nested looping over the same iterable.
let index = 0;
return {
// Note: using an arrow function allows `this` to point to the
// one of `[Symbol.iterator]()` instead of `next()`
next: () => {
if (index >= this.#data.length) {
return { done: true };
}
return { value: this.#data[index++], done: false };
},
};
}
}
const simple = new SimpleClass([1, 2, 3, 4, 5]);
for (const val of simple) {
console.log(val); // 1 2 3 4 5
}
Eingebaute Iterables überschreiben
Beispielsweise ist ein String
ein eingebautes iterierbares Objekt:
const someString = "hi";
console.log(typeof someString[Symbol.iterator]); // "function"
Der Standard-Iterator von String
gibt die Codepunkte der Zeichenkette nacheinander zurück:
const iterator = someString[Symbol.iterator]();
console.log(`${iterator}`); // "[object String Iterator]"
console.log(iterator.next()); // { value: "h", done: false }
console.log(iterator.next()); // { value: "i", done: false }
console.log(iterator.next()); // { value: undefined, done: true }
Sie können das Iterationsverhalten neu definieren, indem Sie Ihren eigenen [Symbol.iterator]()
bereitstellen:
// need to construct a String object explicitly to avoid auto-boxing
const someString = new String("hi");
someString[Symbol.iterator] = function () {
return {
// this is the iterator object, returning a single element (the string "bye")
next() {
return this._first
? { value: "bye", done: (this._first = false) }
: { done: true };
},
_first: true,
};
};
Beachten Sie, wie sich das Neudefinieren von [Symbol.iterator]()
auf das Verhalten von eingebauten Konstrukten auswirkt, die das Iterationsprotokoll verwenden:
console.log([...someString]); // ["bye"]
console.log(`${someString}`); // "hi"
Gleichzeitige Änderungen beim Iterieren
Fast alle Iterables haben die gleiche zugrundeliegende Semantik: Sie kopieren die Daten nicht zu dem Zeitpunkt, zu dem die Iteration beginnt. Stattdessen halten sie einen Zeiger und bewegen ihn hin und her. Wenn Sie also Elemente in der Sammlung hinzufügen, löschen oder ändern, während Sie über die Sammlung iterieren, können Sie unbeabsichtigt ändern, ob andere unveränderte Elemente in der Sammlung besucht werden. Dies ist sehr ähnlich wie bei iterativen Array-Methoden.
Betrachten Sie den folgenden Fall mit einem URLSearchParams
:
const searchParams = new URLSearchParams(
"deleteme1=value1&key2=value2&key3=value3",
);
// Delete unwanted keys
for (const [key, value] of searchParams) {
console.log(key);
if (key.startsWith("deleteme")) {
searchParams.delete(key);
}
}
// Output:
// deleteme1
// key3
Beachten Sie, wie es niemals key2
ausgibt. Dies liegt daran, dass ein URLSearchParams
zugrunde liegend eine Liste von Schlüssel-Wert-Paaren ist. Wenn deleteme1
besucht und gelöscht wird, werden alle anderen Einträge um eins nach links verschoben, sodass key2
die Position einnimmt, die deleteme1
früher hatte, und wenn der Zeiger zum nächsten Schlüssel geht, landet er auf key3
.
Bestimmte Iterable-Implementierungen vermeiden dieses Problem, indem sie "Grabstein"-Werte setzen, um zu vermeiden, dass die verbleibenden Werte verschoben werden. Betrachten Sie den ähnlichen Code mit einer Map
:
const myMap = new Map([
["deleteme1", "value1"],
["key2", "value2"],
["key3", "value3"],
]);
for (const [key, value] of myMap) {
console.log(key);
if (key.startsWith("deleteme")) {
myMap.delete(key);
}
}
// Output:
// deleteme1
// key2
// key3
Beachten Sie, wie es alle Schlüssel ausgibt. Dies liegt daran, dass Map
die verbleibenden Schlüssel nicht verschiebt, wenn einer gelöscht wird. Wenn Sie etwas Ähnliches implementieren möchten, könnte es so aussehen:
const tombstone = Symbol("tombstone");
class MyIterable {
#data;
constructor(data) {
this.#data = data;
}
delete(deletedKey) {
for (let i = 0; i < this.#data.length; i++) {
if (this.#data[i][0] === deletedKey) {
this.#data[i] = tombstone;
return true;
}
}
return false;
}
*[Symbol.iterator]() {
for (const data of this.#data) {
if (data !== tombstone) {
yield data;
}
}
}
}
const myIterable = new MyIterable([
["deleteme1", "value1"],
["key2", "value2"],
["key3", "value3"],
]);
for (const [key, value] of myIterable) {
console.log(key);
if (key.startsWith("deleteme")) {
myIterable.delete(key);
}
}
Warnung: Gleichzeitige Änderungen sind im Allgemeinen sehr fehleranfällig und verwirrend. Sofern Sie nicht genau wissen, wie das Iterable implementiert ist, ist es am besten, die Sammlung während des Iterierens nicht zu ändern.
Spezifikationen
Specification |
---|
ECMAScript® 2026 Language Specification # sec-iteration |